﻿using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;

namespace eventModel
{
    public static class Compiler
    {
        public static int MaxAllowedCachedCompilations { get; set; }

        /// <summary>Represents a cached executable compilation.</summary>
        private class CachedCompilation
        {
            public int Executions { get; set; } // Counts execution of cached reference.
            public object ObjectHandle { get; set; } // Handle to object instance
            public MethodInfo MethodHandle { get; set; } // Handle to method
            public DateTime LastCompilation { get; set; } // Specifies the last time the hash was generated.
        }

        //Keeps an in-memory hash table reference of all our compiled objects with their method handles.
        private static Dictionary<string, CachedCompilation> CachedCompilations = new Dictionary<string, CachedCompilation>();

        
        /// <summary>Cleans up the object cache if we've reached our max</summary>
        private static void CleanObjectCache(object parameter)
        {
            // Crude implemenation of "cleaning memory" of old objects if we cache more than the
            // maximun allows cached compilations:
            if (CachedCompilations.Count > (MaxAllowedCachedCompilations - 1))
            {
                Logging.Add("SERVER - Max allowed cached compilations hit, cleaning up.");

                // This isn't tested...
                CachedCompilations.ToList().OrderByDescending(c => c.Value.LastCompilation).
                    Take(CachedCompilations.Count - (MaxAllowedCachedCompilations - 5)).ToList().
                        ForEach(c => CachedCompilations.Remove(c.Key));
            }
        }

        /// <summary>Updates the compilation cache as it relates to a specific file</summary>
        public static void UpdateObjectCache(string fileName)
        {
            Logging.Add("SERVER - Updating compilation cache for '" + fileName + "'");
            if (CachedCompilations.ContainsKey(fileName))
            {
                // If our compilation is old, remove that object:
                if (CachedCompilations[fileName].LastCompilation < DateTime.Now.AddMilliseconds(-500))
                { CachedCompilations.Remove(fileName); }
                else { return; }
            }

            // Run compiler and cache new changes.
            Compile(fileName, null, false);
        }

        
        /// <summary> Retrieves results from an executable file. Will attemt to use a cached compilation first,
        /// if none exists, will compile the executable, store the cache, and call itself to execute the results.
        /// </summary><param name="fileName">File name to execute</param><param name="ImmidiateExecution">If set 
        /// to false, will compile the file and add to object cache but will not retrieve the results</param>
        public static string Compile(string fileName, Context cntxt, bool ImmidiateExecution = true)
        {
            // First see if we have a chached compilation for this file:
            if (CachedCompilations.ContainsKey(fileName))
            {
                try
                {
                    // If so, attempt to execute it:
                    object s = CachedCompilations[fileName].MethodHandle.Invoke(CachedCompilations[fileName].ObjectHandle, new object[] { cntxt });
                    CachedCompilations[fileName].Executions++; // update executions:
                    return s.ToString(); // return results
                }
                catch (Exception ce) // Obviously it compiled fine in the past, so if we bomb, it's a runtime error:
                { return "<html><title>501 ERROR</title><body><h1>501</h1>RUNTIME ERROR: " + ce.Message + "<body></html>"; }
            }

            // Set up compiler:
            CSharpCodeProvider c = new CSharpCodeProvider();
            CompilerParameters cp = new CompilerParameters();

            // Import references:
            cp.ReferencedAssemblies.Add("system.dll");
            cp.ReferencedAssemblies.Add("system.data.dll");

            // Compile in memory:
            cp.CompilerOptions = "/t:library";
            cp.GenerateInMemory = true;

            try
            {
                // PA: Changed this so that now it is first passed to a parser before compilation...
                //TODO: Parser should be smarter about returning readable error messages. (e.g. "Missing bracket")
                string FinalCompilement = Parser.Process(fileName);

                Logging.Add("SERVER - Compiling '" + fileName + "'");

                // Try compiling:
                CompilerResults cr = c.CompileAssemblyFromSource(cp, FinalCompilement);

                // If failed, return compilation error:
                if (cr.Errors.Count > 0)
                { return "<html><title>501 ERROR</title><body><h1>501</h1>COMPILE ERROR: " + cr.Errors[0].ErrorText + "<body></html>"; }

                // Otherwise create our handles & instance:
                System.Reflection.Assembly a = cr.CompiledAssembly;
                object o = a.CreateInstance("CSCodeEvaler.CSCodeEvaler");
                Type t = o.GetType();
                MethodInfo mi = t.GetMethod("EvalCode");

                // Add cached compilation object:
                CachedCompilation cc = new CachedCompilation();
                cc.Executions = 0;
                cc.ObjectHandle = o;
                cc.MethodHandle = mi;
                cc.LastCompilation = DateTime.Now;
                CachedCompilations.Add(fileName, cc);

                // Fire thread to clean up data if we're nearing our max number of allowed cached compilations:
                if (CachedCompilations.Count > (MaxAllowedCachedCompilations - 1))
                { ThreadPool.QueueUserWorkItem(new WaitCallback(CleanObjectCache), null); }

                // In normal operation, call this method again knowing this time we'll have the compilation cached:
                if (ImmidiateExecution) { return Compile(fileName, cntxt); } else { return "Compilation Success"; }
            }
            catch (Exception ce) // Handle errors:
            { return "<html><title>501 ERROR</title><body><h1>501</h1>COMPILE/CACHING ERROR: " + ce.Message + "<body></html>"; }
        }
    }
}
